page-data-props.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
  2. import type {
  3. IDataWithRequiredMeta,
  4. IPage,
  5. IPageInfoBasic,
  6. IPageNotFoundInfo,
  7. IUser,
  8. } from '@growi/core';
  9. import { isIPageInfo, isIPageNotFoundInfo } from '@growi/core';
  10. import {
  11. isPermalink as _isPermalink,
  12. isTopPage,
  13. } from '@growi/core/dist/utils/page-path-utils';
  14. import { removeHeadingSlash } from '@growi/core/dist/utils/path-utils';
  15. import assert from 'assert';
  16. import type { HydratedDocument, model } from 'mongoose';
  17. import type { CrowiRequest } from '~/interfaces/crowi-request';
  18. import type { PageDocument, PageModel } from '~/server/models/page';
  19. import type {
  20. IPageRedirect,
  21. PageRedirectModel,
  22. } from '~/server/models/page-redirect';
  23. import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
  24. import type { CommonEachProps } from '../common-props';
  25. import type {
  26. GeneralPageInitialProps,
  27. IPageToShowRevisionWithMeta,
  28. } from '../general-page';
  29. import type { EachProps } from './types';
  30. // Utility to resolve path, redirect, and identical path page check
  31. type PathResolutionResult = {
  32. resolvedPagePath: string;
  33. isIdenticalPathPage: boolean;
  34. redirectFrom?: string;
  35. };
  36. let mongooseModel: typeof model;
  37. let Page: PageModel;
  38. let PageRedirect: PageRedirectModel;
  39. async function initModels(): Promise<void> {
  40. if (mongooseModel == null) {
  41. mongooseModel = (await import('mongoose')).model;
  42. }
  43. if (Page == null) {
  44. Page = mongooseModel<IPage, PageModel>('Page');
  45. }
  46. if (PageRedirect == null) {
  47. PageRedirect = mongooseModel<IPageRedirect, PageRedirectModel>(
  48. 'PageRedirect',
  49. );
  50. }
  51. }
  52. async function resolvePathAndCheckIdentical(
  53. path: string,
  54. user: IUser | undefined,
  55. ): Promise<PathResolutionResult> {
  56. await initModels();
  57. const isPermalink = _isPermalink(path);
  58. let resolvedPagePath = path;
  59. let redirectFrom: string | undefined;
  60. let isIdenticalPathPage = false;
  61. if (!isPermalink) {
  62. const chains = await PageRedirect.retrievePageRedirectEndpoints(path);
  63. if (chains != null) {
  64. resolvedPagePath = chains.end.toPath;
  65. redirectFrom = chains.start.fromPath;
  66. }
  67. const multiplePagesCount = await Page.countByPathAndViewer(
  68. resolvedPagePath,
  69. user,
  70. null,
  71. true,
  72. );
  73. isIdenticalPathPage = multiplePagesCount > 1;
  74. }
  75. return { resolvedPagePath, isIdenticalPathPage, redirectFrom };
  76. }
  77. /**
  78. * Convert pathname based on page data and permalink status
  79. * @returns Final pathname to be used in the URL
  80. */
  81. function resolveFinalizedPathname(
  82. pagePath: string,
  83. page: HydratedDocument<IPage> | null | undefined,
  84. isPermalink: boolean,
  85. ): string {
  86. let finalPathname = pagePath;
  87. if (page != null) {
  88. // /62a88db47fed8b2d94f30000 ==> /path/to/page
  89. if (isPermalink && page.isEmpty) {
  90. finalPathname = page.path;
  91. }
  92. // /path/to/page ==> /62a88db47fed8b2d94f30000
  93. if (!isPermalink && !page.isEmpty) {
  94. const isToppage = isTopPage(pagePath);
  95. if (!isToppage && page._id) {
  96. finalPathname = `/${page._id.toString()}`;
  97. }
  98. }
  99. }
  100. return finalPathname;
  101. }
  102. // Page data retrieval for initial load - returns GetServerSidePropsResult
  103. export async function getPageDataForInitial(
  104. context: GetServerSidePropsContext,
  105. ): Promise<
  106. GetServerSidePropsResult<
  107. Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> &
  108. Pick<
  109. EachProps,
  110. 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'
  111. >
  112. >
  113. > {
  114. const req: CrowiRequest = context.req as CrowiRequest;
  115. const { crowi, user } = req;
  116. const { revisionId } = req.query;
  117. // Parse path from URL
  118. let { path: pathFromQuery } = context.query;
  119. pathFromQuery = pathFromQuery != null ? (pathFromQuery as string[]) : [];
  120. let pathFromUrl = `/${pathFromQuery.join('/')}`;
  121. pathFromUrl = pathFromUrl === '//' ? '/' : pathFromUrl;
  122. const { pageService, pageGrantService, configManager } = crowi;
  123. const pageId = _isPermalink(pathFromUrl)
  124. ? removeHeadingSlash(pathFromUrl)
  125. : null;
  126. const isPermalink = _isPermalink(pathFromUrl);
  127. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  128. await resolvePathAndCheckIdentical(pathFromUrl, user);
  129. if (isIdenticalPathPage) {
  130. return {
  131. props: {
  132. currentPathname: resolvedPagePath,
  133. isIdenticalPathPage: true,
  134. pageWithMeta: null,
  135. skipSSR: false,
  136. redirectFrom,
  137. },
  138. };
  139. }
  140. // Get full page data
  141. const pageWithMeta = await findPageAndMetaDataByViewer(
  142. pageService,
  143. pageGrantService,
  144. { pageId, path: resolvedPagePath, user },
  145. );
  146. // Handle URL conversion
  147. const currentPathname = resolveFinalizedPathname(
  148. resolvedPagePath,
  149. pageWithMeta.data,
  150. isPermalink,
  151. );
  152. // When the page exists
  153. if (pageWithMeta.data != null) {
  154. const { data: page, meta } = pageWithMeta;
  155. // type assertion
  156. assert(isIPageInfo(meta), 'meta should be IPageInfo when data is not null');
  157. // Handle empty pages - return as not found to avoid serialization issues
  158. if (page.isEmpty) {
  159. return {
  160. props: {
  161. currentPathname,
  162. isIdenticalPathPage: false,
  163. pageWithMeta: {
  164. data: null,
  165. meta: {
  166. isNotFound: true,
  167. isForbidden: false,
  168. },
  169. } satisfies IDataWithRequiredMeta<null, IPageNotFoundInfo>,
  170. skipSSR: false,
  171. redirectFrom,
  172. },
  173. };
  174. }
  175. // Handle existing page with valid meta that is not IPageNotFoundInfo
  176. page.initLatestRevisionField(revisionId);
  177. const ssrMaxRevisionBodyLength = configManager.getConfig(
  178. 'app:ssrMaxRevisionBodyLength',
  179. );
  180. // Check if SSR should be skipped
  181. const latestRevisionBodyLength = await page.getLatestRevisionBodyLength();
  182. const skipSSR =
  183. latestRevisionBodyLength != null &&
  184. ssrMaxRevisionBodyLength < latestRevisionBodyLength;
  185. const populatedPage = await page.populateDataToShowRevision(skipSSR);
  186. return {
  187. props: {
  188. currentPathname,
  189. isIdenticalPathPage: false,
  190. pageWithMeta: {
  191. data: populatedPage,
  192. meta,
  193. } satisfies IPageToShowRevisionWithMeta,
  194. skipSSR,
  195. redirectFrom,
  196. },
  197. };
  198. }
  199. // type assertion
  200. assert(
  201. isIPageNotFoundInfo(pageWithMeta.meta),
  202. 'meta should be IPageNotFoundInfo when data is null',
  203. );
  204. // Handle the case where the page does not exist
  205. return {
  206. props: {
  207. currentPathname: resolvedPagePath,
  208. isIdenticalPathPage: false,
  209. pageWithMeta: pageWithMeta satisfies IDataWithRequiredMeta<
  210. null,
  211. IPageNotFoundInfo
  212. >,
  213. skipSSR: false,
  214. redirectFrom,
  215. },
  216. };
  217. }
  218. // Page data retrieval for same-route navigation
  219. export async function getPageDataForSameRoute(
  220. context: GetServerSidePropsContext,
  221. ): Promise<{
  222. props: Pick<CommonEachProps, 'currentPathname'> &
  223. Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>;
  224. internalProps?: {
  225. pageWithMeta?:
  226. | IDataWithRequiredMeta<PageDocument, IPageInfoBasic>
  227. | IDataWithRequiredMeta<null, IPageNotFoundInfo>;
  228. };
  229. }> {
  230. const req: CrowiRequest = context.req as CrowiRequest;
  231. const { crowi, user } = req;
  232. const { pageService, pageGrantService } = crowi;
  233. const pathname = decodeURIComponent(
  234. context.resolvedUrl?.split('?')[0] ?? '/',
  235. );
  236. const pageId = _isPermalink(pathname) ? removeHeadingSlash(pathname) : null;
  237. const isPermalink = _isPermalink(pathname);
  238. const { resolvedPagePath, isIdenticalPathPage, redirectFrom } =
  239. await resolvePathAndCheckIdentical(pathname, user);
  240. if (isIdenticalPathPage) {
  241. return {
  242. props: {
  243. currentPathname: resolvedPagePath,
  244. isIdenticalPathPage: true,
  245. redirectFrom,
  246. },
  247. };
  248. }
  249. // For same route access, do minimal page lookup
  250. const pageWithMetaBasicOnly = await findPageAndMetaDataByViewer(
  251. pageService,
  252. pageGrantService,
  253. { pageId, path: resolvedPagePath, user, basicOnly: true },
  254. );
  255. const currentPathname = resolveFinalizedPathname(
  256. resolvedPagePath,
  257. pageWithMetaBasicOnly.data,
  258. isPermalink,
  259. );
  260. return {
  261. props: {
  262. currentPathname,
  263. isIdenticalPathPage: false,
  264. redirectFrom,
  265. },
  266. internalProps: {
  267. pageWithMeta: pageWithMetaBasicOnly.data?.isEmpty
  268. ? {
  269. data: null,
  270. meta: { isNotFound: true, isForbidden: false },
  271. }
  272. : pageWithMetaBasicOnly,
  273. },
  274. };
  275. }